package it.unibo.refolding.pico;

import it.unibo.refolding.alg.NoisyFunction;
import it.unibo.refolding.common.CommonThings;
import it.unibo.refolding.common.DoublePair;
import it.unibo.refolding.common.IntPair;
import it.unibo.refolding.pico.PicoDataAnalyser.ContactPointAndSlope;
import it.unibo.refolding.pico.stripchart.AnalysisOptions;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.Parser;
import org.apache.commons.lang3.StringUtils;

import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;

public class CycleAnalysis {

  private static final Logger logger = Logger.getLogger(CycleAnalysis.class.getSimpleName());

  public static void main(String[] args) throws ParseException, IOException {
    System.setProperty("java.awt.headless", "true");

    Options options = new Options();

    Option dataDirOption = PicoUtil.createDataDirOption();
    options.addOption(dataDirOption);

    Option inputPathListOption = PicoUtil.createInputPathListOption();
    options.addOption(inputPathListOption);

    Option contactPointAndSlopeDirOption = ContactPointAndSlopeHelper.createContactPointAndSlopeDirOption();
    options.addOption(contactPointAndSlopeDirOption);

    Option peakSlopesDirOption = PeakSlopesHelper.createPeakSlopesDirOption();
    options.addOption(peakSlopesDirOption);

    Option wildcardPatternOption = PicoUtil.createWildcardPatternOption();
    options.addOption(wildcardPatternOption);

    Option fileFormatOption = AnalysisOptions.createFileFormatOption();
    options.addOption(fileFormatOption);

    Option cycleSummaryPathOption = new Option("cycleSummaryPath", true, "");
    cycleSummaryPathOption.setRequired(true);
    options.addOption(cycleSummaryPathOption);

    Parser parser = new GnuParser();
    CommandLine line = parser.parse(options, args);

    FileFormat fileFormat = FileFormat.valueOf(line.getOptionValue(
      fileFormatOption.getOpt(), FileFormat.NANOSCOPE.name()).toUpperCase(Locale.ROOT));
    String wildcardPattern = line.getOptionValue(wildcardPatternOption.getOpt());
    Path contactPointAndSlopeDir = Paths.get(line.getOptionValue(contactPointAndSlopeDirOption.getOpt()));
    Path peakSlopesDir = Paths.get(line.getOptionValue(peakSlopesDirOption.getOpt()));
    String dataDir = line.getOptionValue(dataDirOption.getOpt());
    String inputPathList = line.getOptionValue(inputPathListOption.getOpt());
    Preconditions.checkArgument(
      StringUtils.isEmpty(dataDir) ^ StringUtils.isEmpty(inputPathList),
      "Must specify exactly one of dataDir and inputPathList.");
    Path cycleSummaryPath = Paths.get(line.getOptionValue(cycleSummaryPathOption.getOpt()));

    List<Path> files = PicoUtil.computeFileList(dataDir, inputPathList, wildcardPattern);
    List<CycleSummary> cycleSummaryList = new ArrayList<>();
    for (Path file : files) {
      List<PicoData> picoDataList = safeReadPicoData(fileFormat, file);
      if (picoDataList.size() > 1) {
        List<PlainPicoData> plainPicoDataList = toPlainPicoDataList(picoDataList);
        PicoUtil.checkSameSettings(plainPicoDataList);
        Path contactPointAndSlopeFile = ContactPointAndSlopeHelper.contactPointAndSlopeFile(
          contactPointAndSlopeDir, file.getFileName().toString());
        Path peakSlopesPath = PeakSlopesHelper.peakSlopesFile(
          peakSlopesDir, file.getFileName().toString());
        if (Files.isRegularFile(contactPointAndSlopeFile) && Files.isRegularFile(peakSlopesPath)) {
          ContactPointAndSlope retractionContactPointAndSlope = ContactPointAndSlopeHelper.extractContactPointAndSlope(contactPointAndSlopeFile);
          List<PeakSlopes> peakSlopesList = PeakSlopesHelper.extractPeakSlopesList(peakSlopesPath);
          List<IterationInfo> iterations = new ArrayList<>();
          for (int i = 0; i < plainPicoDataList.size(); ++i) {
            iterations.add(iterationInfo(
              retractionContactPointAndSlope, peakSlopesList.get(i), plainPicoDataList.get(i), file
                + " " + i));
          }
          List<IterationSummary> iterationSummaryList = new ArrayList<>();
          for (int i = 0; i < plainPicoDataList.size(); ++i) {
            IterationInfo iteration = iterations.get(i);
            double distanceDefLsbSquare = iteration.deflectionLsbAfterContactPoint.distance(CommonThings.last(iterations).deflectionLsbAfterContactPoint);
            iterationSummaryList.add(new IterationSummary(iteration.lastPeakZLsb
              * plainPicoDataList.get(i).zLsbToMetresFactor, iteration.totalExtensionZLsb
              * plainPicoDataList.get(i).zLsbToMetresFactor, distanceDefLsbSquare
              * plainPicoDataList.get(i).deflectionLsbToNewtonsFactor()));
          }
          cycleSummaryList.add(new CycleSummary(file, iterationSummaryList));
        }
      }
    }
    Files.write(
      cycleSummaryPath,
      (StringUtils.join(cycleSummaryList, '\n') + (cycleSummaryList.isEmpty() ? "" : '\n')).getBytes(Charsets.ISO_8859_1));
    logger.log(
      Level.INFO, "Wrote {0} cycle summaries in: {1}", new Object[] {
      cycleSummaryList.size(), cycleSummaryPath});
  }

  public static List<PicoData> safeReadPicoData(FileFormat fileFormat, Path file) {
    try {
      return PicoUtil.readPicoData(fileFormat, file, HeaderParserStrategy.JREGEX);
    } catch (RuntimeException ex) {
      logger.log(
        Level.WARNING, "Could not read {0} of file format {1} because of exception {2}.",
        new Object[] {file, fileFormat, ex});
      return ImmutableList.of();
    }
  }

  private static List<PlainPicoData> toPlainPicoDataList(List<PicoData> in) {
    List<PlainPicoData> a = new ArrayList<>();
    for (PicoData b : in) {
      a.add((PlainPicoData) b);
    }
    return a;
  }

  private static class IterationSummary {
    private final double lastPeakExtensionM, maxExtensionM, distanceN;

    public IterationSummary(double lastPeakExtensionM, double maxExtensionM, double distanceN) {
      Preconditions.checkArgument(Double.isNaN(lastPeakExtensionM) | lastPeakExtensionM > 0);
      Preconditions.checkArgument(maxExtensionM >= 0);
      Preconditions.checkArgument(!(lastPeakExtensionM > maxExtensionM));
      Preconditions.checkArgument(distanceN >= 0);
      this.lastPeakExtensionM = lastPeakExtensionM;
      this.maxExtensionM = maxExtensionM;
      this.distanceN = distanceN;
    }

    public String repr() {
      return String.format(
        Locale.ROOT, "IterationSummary(%s, %s, %s)", Double.toString(lastPeakExtensionM),
        Double.toString(maxExtensionM), Double.toString(distanceN));
    }

    @Override
    public String toString() {
      return repr();
    }

    public static String repr(List<IterationSummary> list) {
      return '[' + StringUtils.join(list, ", ") + ']';
    }
  }

  private static class CycleSummary {
    private final Path path;
    private final List<IterationSummary> iterations;

    public CycleSummary(Path path, List<IterationSummary> iterations) {
      Preconditions.checkArgument(CommonThings.last(iterations).distanceN == 0);
      this.path = path;
      this.iterations = iterations;
    }

    public String repr() {
      return String.format(
        Locale.ROOT, "CycleSummary(r'%s', %s)", path, IterationSummary.repr(iterations));
    }

    @Override
    public String toString() {
      return repr();
    }
  }

  private static class IterationInfo {

    public static final IterationInfo EMPTY = new IterationInfo(Double.NaN, 0, NoisyFunction.EMPTY);

    final double lastPeakZLsb, totalExtensionZLsb;
    final NoisyFunction deflectionLsbAfterContactPoint;

    public IterationInfo(double lastPeakZLsb, double totalExtensionZLsb,
      NoisyFunction deflectionLsbAfterContactPoint) {
      this.lastPeakZLsb = lastPeakZLsb;
      this.totalExtensionZLsb = totalExtensionZLsb;
      this.deflectionLsbAfterContactPoint = deflectionLsbAfterContactPoint;
    }
  }

  private static IterationInfo iterationInfo(
    ContactPointAndSlope retractionContactPoint, PeakSlopes peakSlopes,
    PlainPicoData plainPicoData, String message) {
    int offset;
    try {
      offset = PicoDataAnalyser.offsetOfEndingContiguousRegionNotBeforeContactPoint(
        plainPicoData.sharedRetraction().sharedZArray(), retractionContactPoint.zLsb);
    } catch (IllegalArgumentException ex) {
      logger.log(
        Level.WARNING, "Could not find region after contact point: {0}, {1}", new Object[] {
        message, ex});
      return IterationInfo.EMPTY;
    }
    DoublePair lastPeakAndTotalExtensionZLsb = lastPeakAndTotalExtensionZLsb(
      plainPicoData.retraction, retractionContactPoint.zLsb, offset, peakSlopes);
    // No need to deangle to compute distances.
    NoisyFunction f = NoisyFunction.of(Arrays.copyOfRange(
      plainPicoData.sharedRetraction().sharedZArray(), offset,
      plainPicoData.sharedRetraction().sharedZArray().length), Arrays.copyOfRange(
      plainPicoData.sharedRetraction().sharedDeflectionArray(), offset,
      plainPicoData.sharedRetraction().sharedDeflectionArray().length));
    return new IterationInfo(lastPeakAndTotalExtensionZLsb.x, lastPeakAndTotalExtensionZLsb.y, f);
  }

  /**
   * The result must be multiplied by plainPicoData.zLsbToMetresFactor to obtain
   * metres.
   */
  private static DoublePair lastPeakAndTotalExtensionZLsb(
    PlainDirectionData retraction, double retractionContactPointZLsb, int offset,
    PeakSlopes peakSlopes) {
    double totalExtensionZLsb = retractionContactPointZLsb
      - retraction.z(retraction.timeInstantCount() - 1);
    final double lastPeakExtensionZLsb;
    if (peakSlopes.size() == 0) {
      lastPeakExtensionZLsb = Double.NaN;
    } else {
      IntPair startSlopeAndPeakIndex = peakSlopes.get(peakSlopes.size() - 1);
      lastPeakExtensionZLsb = retractionContactPointZLsb
        - retraction.z(offset + startSlopeAndPeakIndex.y);
    }
    return DoublePair.of(lastPeakExtensionZLsb, totalExtensionZLsb);
  }
}
